package scoping;

import ic.ast.Visitor;
import ic.ast.decl.ClassType;
import ic.ast.decl.DeclClass;
import ic.ast.decl.DeclField;
import ic.ast.decl.DeclLibraryMethod;
import ic.ast.decl.DeclMethod;
import ic.ast.decl.DeclStaticMethod;
import ic.ast.decl.DeclVirtualMethod;
import ic.ast.decl.Parameter;
import ic.ast.decl.PrimitiveType;
import ic.ast.decl.PrimitiveType.DataType;
import ic.ast.decl.Program;
import ic.ast.decl.Type;
import ic.ast.expr.BinaryOp;
import ic.ast.expr.Expression;
import ic.ast.expr.ExpressionBlock;
import ic.ast.expr.Length;
import ic.ast.expr.Literal;
import ic.ast.expr.NewArray;
import ic.ast.expr.NewInstance;
import ic.ast.expr.RefArrayElement;
import ic.ast.expr.RefField;
import ic.ast.expr.RefVariable;
import ic.ast.expr.StaticCall;
import ic.ast.expr.This;
import ic.ast.expr.UnaryOp;
import ic.ast.expr.VirtualCall;
import ic.ast.stmt.LocalVariable;
import ic.ast.stmt.StmtAssignment;
import ic.ast.stmt.StmtBlock;
import ic.ast.stmt.StmtBreak;
import ic.ast.stmt.StmtCall;
import ic.ast.stmt.StmtContinue;
import ic.ast.stmt.StmtIf;
import ic.ast.stmt.StmtReturn;
import ic.ast.stmt.StmtWhile;

import java.util.HashMap;
import java.util.List;

import scoping.blocks.ClassScope;
import scoping.blocks.GlobalScope;
import scoping.blocks.MethodScope;
import scoping.blocks.ScopeBlock;
import scoping.blocks.StmtBlockScope;
import scoping.helpers.FaultyType;
import scoping.helpers.FaultyType.FaultType;
import scoping.helpers.VariableLocation;


public class Verifier implements Visitor {
	
	private HashMap<String, String> classMaps;
	
	public Verifier(ScopeTree global) {
		super();
		classMaps = new HashMap<String, String>();
		ScanClassScopeForChildClass(global);
	}

	private void ScanClassScopeForChildClass(ScopeTree classScopeTree){
		ClassScope parent = classScopeTree.getBlock() instanceof ClassScope ? (ClassScope)classScopeTree.getBlock() : null;
		for (ScopeTree tree:classScopeTree.getChildren()){
			if (tree.getBlock() instanceof ClassScope){
				if (parent != null){
					classMaps.put(((ClassScope)tree.getBlock()).getClassDeclaration().getName(), parent.getClassDeclaration().getName());	
				}				
				ScanClassScopeForChildClass(tree);
			}
		}
	}
	
	private DeclField getClassField(DeclClass declClass, String fieldName){
		while (declClass != null){
			for (DeclField declField:declClass.getFields()){
				if (declField.getName().equals(fieldName)){
					return declField;
				}
			}
			ScopeBlock parent = declClass.getScope().getParent().getBlock();
			ClassScope parentClassScope = parent instanceof ClassScope ? (ClassScope)parent : null;
			declClass = parentClassScope != null ? parentClassScope.getClassDeclaration() : null;
		}
		return null;
	}
	
	private Type verifyCondition(int line, Expression condition, String stmt){
		Type t = (Type)condition.accept(this);
		if (t instanceof FaultyType)
			return (FaultyType)t;
		if ((t instanceof PrimitiveType) && 
				((PrimitiveType)t).getDisplayName().equalsIgnoreCase(DataType.BOOLEAN.toString()) 
				&& ((PrimitiveType)t).getArrayDimension() == 0){
			return new PrimitiveType(line, DataType.VOID);
		}
		return new FaultyType(line, 
				String.format(FaultType.NON_BOOLEAN_CONDITION.getDescription(), stmt));
	}
	
	public Object visit(Program program){ 
		return new PrimitiveType(program.getLine(), DataType.VOID);
	}

	public Object visit(DeclClass icClass){
		if (icClass.hasSuperClass())
		{
			VariableLocation classLocation = VariableLocation.getVariableScope(icClass.getScope(), icClass.getSuperClassName());
			if (classLocation == null || (!(classLocation.getType() instanceof ClassType))) {
				return new FaultyType(icClass.getLine(), 
								String.format(FaultType.EXTENDS_NOT_FOUND.getDescription(),
												icClass.getName(),icClass.getSuperClassName() ));
			}
		}
		return new PrimitiveType(icClass.getLine(), DataType.VOID);
	}

	public Object visit(DeclField field)
	{ 
		VariableLocation location = VariableLocation.getVariableScope(field.getScope(), field.getName());
		if (location != null) //this is a sanity check will always be != null
		{
			ScopeBlock block = location.getScopeTree().getBlock();
			if (block instanceof GlobalScope)
			{//can never happen by Grammar
				return null; 
			}else if (block instanceof StmtBlockScope)
			{//already checked that there isnt another field of the same name therefore it's ok 
				return new PrimitiveType(field.getLine(), DataType.VOID);
			}else if (block instanceof MethodScope)
			{//need to verify that is not shadowing a parameter
				DeclMethod declMethod = ((MethodScope)block).getMethodDeclaration();
				for (Parameter parm : declMethod.getFormals())
				{
					if (parm.getName().equals(field.getName()))
					{
						return new FaultyType(field.getLine(), 
								String.format(FaultType.FIELD_SHADOWING.getDescription(), field.getName()));
					}
				}
				return new PrimitiveType(field.getLine(), DataType.VOID);
			}else if (block instanceof ClassScope)
			{	
				//if is a class scope than all parent trees must be classes too , unless root.
				ScopeTree tree = location.getScopeTree().getParent();
				while (tree != null)
				{
					if (tree.getBlock() instanceof ClassScope)
					{
						if (((ClassScope)tree.getBlock()).getVariable(field.getName()) != null)
						{
							if (((ClassScope)tree.getBlock()).containsMethod(field.getName()))
							{
								return new FaultyType(field.getLine(), 
										String.format(FaultType.FIELD_SHADOWING_METHOD.getDescription(), field.getName())); 
							
							}else
							{
								return new FaultyType(field.getLine(), 
										String.format(FaultType.FIELD_SHADOWING.getDescription(), field.getName())); 
							}
						}
					}else if (tree.getBlock() instanceof GlobalScope)
					{
						return new PrimitiveType(field.getLine(), DataType.VOID); 
					}else
					{//can never reach here
						return null;
					}
					
					tree = tree.getParent();
				}
			}
		}
		//could never reach here
		return null;
	}

	public Object visit(DeclVirtualMethod method){ 
		VariableLocation location = VariableLocation.getVariableScope(method.getScope(), method.getName());
		if (location == null) //this is a sanity check will always be != null
		{
			new FaultyType(method.getLine(),String.format(FaultType.SEMANTIC_ERROR.getDescription(),
					"Vritual method is declared by ast but not defined in any scope"));
		}
		VariableLocation locationInParent = VariableLocation.getVariableScope(location.getScopeTree().getParent(), method.getName());
		if (locationInParent == null){
			// No need to check method override.
			return method.getType();
		}
		if (!(locationInParent.getScopeTree().getBlock() instanceof ClassScope))
		{
			new FaultyType(method.getLine(),String.format(FaultType.SEMANTIC_ERROR.getDescription(),
					"method is declared but not in a class"));
		 
		}
		
		ClassScope classScope = (ClassScope) locationInParent.getScopeTree().getBlock();
		if (!classScope.containsMethod(method.getName())) //method is shadowing a field that was declared in super class
		{
			return new FaultyType(method.getLine(), 
					String.format(FaultType.METHOD_SHADOWING.getDescription(), method.getName()));
		}

		DeclMethod superMethod  = classScope.getMethod(method.getName());
		boolean overrideLegal = checkMethodSignatureEquals(method, superMethod);
		if (overrideLegal){
			return method.getType();
		}
		return new FaultyType (method.getLine(),
				String.format(FaultType.METHOD_OVERLOADS.getDescription(), method.getName()));
	}

	private boolean checkMethodSignatureEquals(DeclMethod method, DeclMethod other){
		if (!typeLTE(method.getType(), other.getType(), true)){
			return false;
		}
		if (method.getFormals().size() != other.getFormals().size()){
			return false;
		}
		for (int i=0;i<method.getFormals().size();i++)
		{
			if (!(typeLTE(method.getFormals().get(i).getType(), other.getFormals().get(i).getType(), true))){
				return false;
			}
		}		
		return true;
	}
	
	public Object visit(DeclStaticMethod method){ 
		return method.getType();
	}

	public Object visit(DeclLibraryMethod method){
		if (!(method.getScope().getParent().getBlock() instanceof ClassScope)
			|| !((ClassScope)method.getScope().getParent().getBlock()).getClassDeclaration().getName().equals("Library")){
			return new FaultyType(method.getLine(), FaultyType.FaultType.CLASS_NOT_FOUND.getDescription());
			// If library method isn't declared in a library class
		}
		return method.getType();
	}

	public Object visit(Parameter formal){ 
		return formal.getType();
	}

	public Object visit(PrimitiveType type){ 
		return type;
	}

	public Object visit(ClassType type){ 
		VariableLocation classLocation = VariableLocation.getVariableScope(type.getScope(), type.getClassName());
		if (classLocation == null || (!(classLocation.getType() instanceof ClassType))) {
			return new FaultyType(type.getLine(),
					String.format(FaultType.CLASS_NOT_FOUND.getDescription(), type.getClassName()));
		}
			
		return type;
	}

	public Object visit(StmtAssignment assignment){
		Type leftType = (Type)assignment.getVariable().accept(this);
		Type rightType = (Type)assignment.getAssignment().accept(this);
		if (leftType instanceof FaultyType){
			return leftType;
		} else if (rightType instanceof FaultyType){
			return rightType;
		} else if (!typeLTE(rightType, leftType)){
			String fType = (rightType.getDisplayName().equals(DataType.INT.toString())) ? "integer" : rightType.getDisplayName();
			String sType = (leftType.getDisplayName().equals(DataType.INT.toString())) ? "integer" : leftType.getDisplayName();
			return new FaultyType(assignment.getLine(),
					String.format(FaultType.INVALID_ASSIGNMENT.getDescription(), 
							fType,sType));
		} else {
			return new PrimitiveType(assignment.getLine(), DataType.VOID);
		}
	}

	public Object visit(StmtCall callStatement){
		return new PrimitiveType(callStatement.getLine(), DataType.VOID);	 
	}

	public Object visit(StmtReturn returnStatement){
		ScopeTree tree = returnStatement.getScope(); 
		while (tree != null){
			if ((tree.getBlock() instanceof StmtBlockScope)){
				tree = tree.getParent();
				continue;
			} 
			if ((tree.getBlock() instanceof MethodScope)){
				break;
			}
			return new FaultyType(returnStatement.getLine(), FaultType.RETURN_NOT_IN_METHOD.getDescription());
		}
		DeclMethod method = ((MethodScope)tree.getBlock()).getMethodDeclaration();
		// verify that the return value exists
		if (! returnStatement.hasValue()){
			if (method.getType().getDisplayName().equalsIgnoreCase(DataType.VOID.toString())){
				return method.getType();
			}
		} else {			
			Type returnType = (Type)returnStatement.getValue().accept(this);
			if (typeLTE(returnType, method.getType())){
				return returnType;
			}
		}
		String typeName = (method.getType().getDisplayName().equals(DataType.INT.toString())) ? "integer" : method.getType().getDisplayName();
		return new FaultyType(returnStatement.getLine(),
				String.format(FaultType.RETURN_VAULE_TYPE.getDescription(), typeName));
	}

	public Object visit(StmtIf ifStatement){ 
		return verifyCondition(ifStatement.getLine(), ifStatement.getCondition(),"if");
	}

	public Object visit(StmtWhile whileStatement)
	{ 
		return verifyCondition(whileStatement.getLine(), whileStatement.getCondition(),"while");
	}

	public Object visit(StmtBreak breakStatement){
		ScopeTree tree = breakStatement.getScope();
		while (tree != null && !(tree.getBlock() instanceof MethodScope)){
			if (tree.getBlock() instanceof StmtBlockScope &&
					((StmtBlockScope)tree.getBlock()).getIsWhile()){
				return new PrimitiveType(breakStatement.getLine(),DataType.VOID) ;
			}
			tree = tree.getParent();
		}
		return new FaultyType(breakStatement.getLine(),FaultType.BREAK.getDescription());		
	}

	public Object visit(StmtContinue continueStatement){
		ScopeTree tree = continueStatement.getScope();
		while (tree != null && !(tree.getBlock() instanceof MethodScope)){
			if (tree.getBlock() instanceof StmtBlockScope &&
					((StmtBlockScope)tree.getBlock()).getIsWhile()){
				return new PrimitiveType(continueStatement.getLine(),DataType.VOID) ;
			}
			tree = tree.getParent();
		}
		return new FaultyType(continueStatement.getLine(),FaultType.CONTINUE.getDescription());
	}

	public Object visit(StmtBlock statementsBlock){ 
		return new PrimitiveType(statementsBlock.getLine(), DataType.VOID);
	}

	public Object visit(LocalVariable localVariable){
		if (localVariable.getType() instanceof ClassType && 
				VariableLocation.getVariableScope(localVariable.getScope(),((ClassType)localVariable.getType()).getClassName()) == null){
			return new FaultyType(localVariable.getLine(), 
							String.format(FaultType.CLASS_NOT_FOUND.getDescription(),
							((ClassType)localVariable.getType()).getClassName()));
		}
		if (localVariable.isInitialized()){
			Type initValueType = (Type)localVariable.getInitialValue().accept(this);
			if (!typeLTE(initValueType, localVariable.getType())){
				String fType = (initValueType.getDisplayName().equals(DataType.INT.toString())) ? "integer" : initValueType.getDisplayName();
				String sType = (localVariable.getType().getDisplayName().equals(DataType.INT.toString())) ? "integer" : localVariable.getType().getDisplayName();
				return new FaultyType(localVariable.getLine(), 
						String.format(FaultType.INVALID_ASSIGNMENT.getDescription(),
										fType,sType));
			}
		}
		if (localVariable.getScope().getBlock() instanceof MethodScope)
		{
			DeclMethod declMethod = ((MethodScope)localVariable.getScope().getBlock()).
					getMethodDeclaration();
			for (Parameter parm : declMethod.getFormals())
			{
				if (parm.getName().equals(localVariable.getName()))
				{
					return new FaultyType(localVariable.getLine(), 
							String.format(FaultType.LOCAL_VAR_SHADOWING.getDescription(), localVariable.getName()));
				}
			}
		}
		
		return localVariable.getType();
	}

	public Object visit(RefVariable location){ 
		VariableLocation varLocation = null;
		int line = location.getLine();
		if ((varLocation = VariableLocation.getVariableScope(location.getScope(), location.getName())) == null)
		{
			return new FaultyType(line,
							String.format(FaultType.NOT_FOUND.getDescription(), location.getName()));
		}
		if (varLocation.getScopeTree().getBlock() instanceof ClassScope){
			// If the refered variable is a field check if it's used within a static method
			ScopeTree tree = location.getScope();
			while (tree != null) {
				// Check if field is reference in static method
				if (tree.getBlock() instanceof MethodScope && 
						((MethodScope)tree.getBlock()).getMethodDeclaration() instanceof DeclStaticMethod)
				{
						return new FaultyType(location.getLine(), FaultType.FIELD_IN_STATIC.getDescription());
				}
				tree = tree.getParent();
			}
		}
		return varLocation.getType();		
	}

	public Object visit(RefField location){ 
		Type type = (Type)location.getObject().accept(this);
		if (type instanceof FaultyType){
			return type;
		}
		if (type instanceof ClassType){
			ClassType classType = (ClassType)type;
			VariableLocation varLocation = VariableLocation.getVariableScope(location.getScope(), classType.getClassName());
			DeclClass declClass = null;
			ClassScope parentClassScope = null;
			if (varLocation.getScopeTree().getBlock() instanceof GlobalScope){
				GlobalScope global = (GlobalScope)varLocation.getScopeTree().getBlock();
				declClass = global.getClass(classType.getClassName());
			} else if (varLocation.getScopeTree().getBlock() instanceof ClassScope) {
				parentClassScope = (ClassScope)varLocation.getScopeTree().getBlock();
				declClass = parentClassScope.getDerivingClass(classType.getClassName());
			}
			DeclField retField = getClassField(declClass, location.getField());
			if (retField != null){
				return retField.getType();
			}
		}
		return new FaultyType(location.getLine(), 
						String.format(FaultType.NOT_FOUND.getDescription(), location.getField()));
	}

	public Object visit(RefArrayElement location)
	{
		Type type = (Type)location.getArray().accept(this);
		if (type instanceof FaultyType){
			return type;
		}
		Type idxType = (Type)location.getIndex().accept(this);
		if (type instanceof FaultyType){
			return idxType;
		}
		if (idxType instanceof PrimitiveType // Make sure index is numeric
				&& ((PrimitiveType)idxType).getDisplayName().equalsIgnoreCase(DataType.INT.toString()))
		{
			return type.createLowerType(1);
		}
		
		return new FaultyType(location.getLine(), 
				String.format(FaultType.SEMANTIC_ERROR.getDescription(), ""));
	}

	public Object visit(StaticCall call){
		DeclMethod method;
		
		// verify existence of class  
		VariableLocation varLocation = VariableLocation.getVariableScope(call.getScope(), call.getClassName());
		if (varLocation == null || !(varLocation.getType() instanceof ClassType)) 
		{ 
			return new FaultyType(call.getLine(), 
					String.format(FaultType.METHOD_DONT_EXISTS.getDescription(), call.getMethod()));
		}
		
		// verify that the method is declared
		if (!(varLocation.getScopeTree().getBlock() instanceof GlobalScope)){
			return new FaultyType(call.getLine(),  
							String.format(FaultType.CLASS_NOT_FOUND.getDescription(), call.getClassName()));
		}
		DeclClass methodClass = ((GlobalScope)varLocation.getScopeTree().getBlock()).getClass(call.getClassName());
		method = VariableLocation.getClassMethod(methodClass, call.getMethod());
		
		if (method == null)
		{ 
			return new FaultyType(call.getLine(), 
							String.format(FaultType.METHOD_NOT_IN_TYPE_TABLE.getDescription(), call.getClassName() + "." + call.getMethod()));
		}
		
		if (call.getArguments().size() != method.getFormals().size()){
			return new FaultyType(call.getLine(), 
					String.format(FaultType.INVALID_NUMBER_OF_ARGS.getDescription(), call.getClassName() + "." + call.getMethod()));
		}
		
		if (verifyArguments(call.getArguments(), method.getFormals()))
		{
			return method.getType();
		}
		return new FaultyType(call.getLine(), 
						String.format(FaultType.ARGS_NOT_VALID.getDescription(), call.getMethod()));
	}

	public Object visit(VirtualCall call)
	{
		DeclMethod method;
		boolean isSuspectedVirtualInStatic = false;

		String methodName = "";
		if (call.hasExplicitObject()){
			Type callObjectType = ((Type)call.getObject().accept(this));
			if (!(callObjectType instanceof ClassType)) 
			{ 
				return new FaultyType(call.getLine(), 
								String.format(FaultType.METHOD_DONT_EXISTS.getDescription(), call.getMethod()));
			}
			String className = ((ClassType)callObjectType).getClassName();
			VariableLocation classVarLocation = VariableLocation.getVariableScope(call.getScope(), className);
			
			if (!(classVarLocation.getScopeTree().getBlock() instanceof GlobalScope)){
				return new FaultyType(call.getLine(),  
								String.format(FaultType.CLASS_NOT_FOUND.getDescription(), className));
			}
			DeclClass methodClass = ((GlobalScope)classVarLocation.getScopeTree().getBlock()).getClass(className);
			method = VariableLocation.getClassMethod(methodClass, call.getMethod());
			methodName = className + "." + call.getMethod();;
		} else {
			ScopeTree tree = call.getScope();
			while (tree != null) {
				if (tree.getBlock() instanceof ClassScope){
					break;
				}
				if (tree.getBlock() instanceof MethodScope)	
				{
					if (((MethodScope)tree.getBlock()).getMethodDeclaration() instanceof DeclStaticMethod)
					{
						isSuspectedVirtualInStatic = true;
					}
				}
				tree = tree.getParent();
			}
			
			if (tree == null)
			{
				return new FaultyType(call.getLine(), FaultType.SEMANTIC_ERROR.getDescription());
			}
			
			methodName = call.getMethod();
			method = VariableLocation.getClassMethod(((ClassScope)tree.getBlock()).getClassDeclaration(), methodName);
			if (isSuspectedVirtualInStatic && method != null && !(method instanceof DeclStaticMethod)){
				return new FaultyType(call.getLine(), FaultType.VIRTUAL_IN_STATIC.getDescription());
			}	
		}
		if (method == null){
			return new FaultyType(call.getLine(), String.format(call.hasExplicitObject() ? 
																FaultType.METHOD_NOT_IN_TYPE_TABLE.getDescription() :
																FaultType.METHOD_NOT_IN_SYMBOL_TABLE.getDescription(), methodName));
		}
		
		if (call.getArguments().size() != method.getFormals().size()){
			return new FaultyType(call.getLine(), 
					String.format(FaultType.INVALID_NUMBER_OF_ARGS.getDescription(), methodName));
		}
		if (verifyArguments(call.getArguments(), method.getFormals()))
		{
			return method.getType();
		}
		return new FaultyType(call.getLine(), 
						String.format(FaultType.ARGS_NOT_VALID.getDescription(), methodName));
	}

	public Object visit(This thisExpression)
	{
		ScopeTree treeNode = thisExpression.getScope();
		ClassScope classScope = null;
		MethodScope methodScope = null;
		
		while (treeNode != null)
		{
			if ((treeNode.getBlock()) instanceof ClassScope)
			{
				classScope= (ClassScope)(treeNode.getBlock());
				return new ClassType(thisExpression.getLine(), classScope.getClassDeclaration().getName());
			}
			
			if ((treeNode.getBlock()) instanceof MethodScope)
			{
				methodScope= (MethodScope)(treeNode.getBlock());
				if (methodScope.getMethodDeclaration() instanceof DeclStaticMethod)
				{
					return new FaultyType(thisExpression.getLine(), FaultType.THIS.getDescription());
				}
			}
			// if stmtblock or virtual method than find parent 
			treeNode = treeNode.getParent();
		}
		
		// if reached here than this is deifned in global which is a mistake there
		// the error message will be specific for us 
		return new FaultyType(thisExpression.getLine(), "this is defined in global scope");
								
	}

	public Object visit(NewInstance newClass)
	{
		VariableLocation classLocation = VariableLocation.getVariableScope(newClass.getScope(), 
				newClass.getName());
		
		if (classLocation == null || !(classLocation.getType() instanceof ClassType)){
			return new FaultyType(newClass.getLine(), 
							String.format(FaultType.CLASS_NOT_FOUND.getDescription(), newClass.getName()));
		}
		
		return classLocation.getType();
	}

	public Object visit(NewArray newArray)
	{ 
		Type sizeExprType = (Type)newArray.getSize().accept(this);
		if (sizeExprType instanceof FaultyType){
			return sizeExprType;
		}
		
		if (!(sizeExprType instanceof PrimitiveType 
				&& ((PrimitiveType)sizeExprType).getDisplayName().equalsIgnoreCase(DataType.INT.toString()))){
			return new FaultyType(newArray.getLine(),
					String.format( FaultType.SEMANTIC_ERROR.getDescription(),"Size expr in new array is not numeric"));
			
		}
		// Make sure size is numeric
		if (newArray.getType() instanceof ClassType) {
			VariableLocation classLocation = VariableLocation.getVariableScope(newArray.getScope(), 
					((ClassType)newArray.getType()).getClassName());
			
			if (classLocation == null){
				return new FaultyType(newArray.getLine(), 
								String.format(FaultType.CLASS_NOT_FOUND.getDescription(),
										((ClassType)newArray.getType()).getClassName()));
			}
		}
		
		return newArray.getType();
	}

	public Object visit(Length length)
	{ 
		//verify that in x.length x is defined
		Object obj = length.getArray().accept(this);
		if (obj instanceof FaultyType){
			return (FaultyType)obj;
		}
		
		//verify that it is an array
		if (((Type)obj).getArrayDimension() == 0){
			return new FaultyType(length.getLine(), 
							String.format(FaultType.NOT_ARRAY.getDescription(),((Type)obj).getDisplayName()));
		}
		
		return new PrimitiveType (length.getLine() , DataType.INT);
	}

	public Object visit(Literal literal)
	{ 
		return new PrimitiveType(literal.getLine(), literal.getType());
	}

	public Object visit(UnaryOp unaryOp)
	{ 
		Object exp = unaryOp.getOperand().accept(this);		
		if (exp instanceof PrimitiveType)
		{
			PrimitiveType expType = (PrimitiveType)exp;
			switch (unaryOp.getOperator().toString())
			{
				case "-":
					if (expType.getDisplayName().equalsIgnoreCase(DataType.INT.toString()))
						return expType;
				case "!":
					if (expType.getDisplayName().equalsIgnoreCase(DataType.BOOLEAN.toString()))
						return expType;
			}
		}
			
		String opType = (unaryOp.getOperator().toString().equals("-")) ? "arithmetic" : "logical";
		String base = (opType.equals("arithmetic")) ? "integer" : "boolean";
       	return new FaultyType(unaryOp.getLine(),
           				String.format(FaultType.OP_USE_NOT_VALID.getDescription(),
               							opType,"unary",unaryOp.getOperator().toString(),base));
	}
	
	public Object visit(BinaryOp binaryOp)
	{ 
		Expression leftOperand = binaryOp.getFirstOperand();
		Expression rightOperand = binaryOp.getSecondOperand();
		int line = binaryOp.getLine();
		
		/* are the expression all declared and used properly */
		Type leftOperandType = (Type)leftOperand.accept(this);
		Type rightOperandType = (Type)rightOperand.accept(this);
		
		if ((leftOperandType instanceof FaultyType) || (rightOperandType instanceof FaultyType) )
		{
			return (leftOperandType instanceof FaultyType) ? leftOperandType : rightOperandType;
		}

		String opType = binaryOp.getOperator().getOpType();
		String base = binaryOp.getOperator().getBaseType();
			
		/* are those type could be compared or workable */
		if (! typeLTE(leftOperandType, rightOperandType))
		{
			return new FaultyType(line , 
					String.format(FaultType.OP_USE_NOT_VALID.getDescription(),
							opType,"binary",binaryOp.getOperator().toString() , base));
		}
		
		switch (binaryOp.getOperator().toString())
		{
			case "+":
				switch(leftOperandType.getDisplayName())
				{
					case "int": 
					case "string": 
						return leftOperandType;
				}
				break;
			case "-":
			case "*":
			case "/":
			case "%":
				if (leftOperandType.getDisplayName().equalsIgnoreCase(DataType.INT.toString()))
					return new PrimitiveType(leftOperand.getLine(), DataType.INT);
				break;
			case "<":
			case "<=":
			case ">":
			case ">=":
					if (leftOperandType.getDisplayName().equalsIgnoreCase(DataType.INT.toString()))
						return new PrimitiveType(leftOperand.getLine(), DataType.BOOLEAN);
					break;
			case "==":
			case "!=":
				return new PrimitiveType(leftOperand.getLine(), DataType.BOOLEAN);
			case "&&":
			case "||":
				if (leftOperandType.getDisplayName().equalsIgnoreCase(DataType.BOOLEAN.toString()))
					return new PrimitiveType(leftOperand.getLine(), DataType.BOOLEAN);
				break;
		}
		return new FaultyType(line, 
						String.format(FaultType.OP_USE_NOT_VALID.getDescription(),
										opType,"binary",binaryOp.getOperator().toString() , base));
	}
	
	public Object visit(ExpressionBlock expressionBlock){ 
		return new PrimitiveType(expressionBlock.getLine(), DataType.VOID);
	}
	
	private boolean typeLTE (Type thisClass , Type expectedClass)
	{
		return typeLTE(thisClass, expectedClass, false);
	}
	
	private boolean typeLTE (Type thisClass , Type expectedClass, boolean onlyEquals)
	{
		if (thisClass instanceof PrimitiveType && expectedClass instanceof PrimitiveType){
			if (expectedClass instanceof PrimitiveType)	{
				return typeLTE((PrimitiveType)thisClass, (PrimitiveType)expectedClass);
			} 
			return expectedClass instanceof PrimitiveType && ((PrimitiveType)expectedClass).getDisplayName().equalsIgnoreCase(DataType.VOID.toString());
			// A == null
		} else if (expectedClass instanceof ClassType){
			if (thisClass instanceof ClassType){
				return typeLTE((ClassType)thisClass, (ClassType)expectedClass, onlyEquals);
			}
			return thisClass instanceof PrimitiveType && ((PrimitiveType)thisClass).getDisplayName().equalsIgnoreCase(DataType.VOID.toString());
			// A = null
		} 
		return false;
	}
	
	private boolean typeLTE (PrimitiveType object , PrimitiveType other)
	{
		boolean sameType = object.getDisplayName().equalsIgnoreCase(other.getDisplayName()) && 
				object.getArrayDimension() == other.getArrayDimension();
		boolean rhsNull = object.getDisplayName().equalsIgnoreCase(DataType.VOID.toString());
		return sameType || ((other.getArrayDimension()  > 0 || other.getDisplayName().equals((DataType.STRING.toString()))) && rhsNull);
	}
	
	private boolean typeLTE (ClassType sonClass  , ClassType superClass, boolean onlyEquals)
	{
		if (sonClass.getArrayDimension() != superClass.getArrayDimension()){
			return false;
		}
		if (sonClass.getClassName().equals(superClass.getClassName())){
			return true;
		}
		if (onlyEquals){
			// If it got this far then the type is not equal.
			return false;
		}

		String searchedClassName = sonClass.getClassName();
		while (classMaps.containsKey(searchedClassName)){
			searchedClassName = classMaps.get(searchedClassName);
			if (searchedClassName.equals(superClass.getClassName())){
				return true;
			}
		}
		return false;
	}
	
	private boolean verifyArguments(List<Expression> argsList , List<Parameter> parameterList)
	{
		int i;
		Type argType;
		List<Parameter> parameters  = parameterList;
		List<Expression> arguments = argsList;
		
		// Verify same amount of arguments
		if (argsList.size() != parameterList.size())
			return false;
		
		for (i=0;i<arguments.size();i++)
		{
			if ((argType = (Type) arguments.get(i).accept(this))  instanceof FaultyType)
				return false;
			if (!(typeLTE(argType, parameters.get(i).getType())))
				return false;	
		}
		
		return true;
	}
}
